feat: store package support#16
Conversation
464d55a to
4ef2c6d
Compare
There was a problem hiding this comment.
Pull request overview
This PR adds initial “store” support to Chisel by introducing a new internal/store package (Snapcraft “bin” store client with caching) and wiring it into the slicer and chisel cut execution path, along with test utilities and unit tests.
Changes:
- Added
internal/storepackage (bin-store API client, URL allowlisting, caching, logging) plus a dedicated test suite. - Extended slicer runtime options and tests to resolve/fetch store-backed packages alongside archive-backed ones (while still failing extraction for store packages).
- Updated CLI wiring to open configured stores and pass them into the slicer, and enabled store logging.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/testutil/store.go | Adds a TestStore fixture implementing the new store.Store interface. |
| internal/testutil/package.go | Extracts TestPackage into a shared fixture type and adds store-related fields. |
| internal/testutil/archive.go | Updates archive test utility to use the shared TestPackage type. |
| internal/store/suite_test.go | Adds gocheck suite bootstrap for the new store package tests. |
| internal/store/store.go | Implements the bin store client (info/fetch/exists) with cache integration and URL validation. |
| internal/store/store_test.go | Adds unit tests for URL validation, Open behavior, Info/Exists/Fetch, caching, and staging env var. |
| internal/store/log.go | Adds logger plumbing consistent with other internal packages. |
| internal/store/export_test.go | Exposes internals for tests (URL validator, env var, HTTP hooks). |
| internal/slicer/slicer.go | Adds store sources to slicer resolution and fetch path (extraction still unsupported). |
| internal/slicer/slicer_test.go | Extends slicer tests to set up store fixtures and updates expected error. |
| cmd/chisel/main.go | Registers store logger in the main CLI. |
| cmd/chisel/cmd_cut.go | Opens configured stores and passes them into the slicer run. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| options.Arch, err = deb.InferArch() | ||
| } else { | ||
| err = deb.ValidateArch(options.Arch) |
There was a problem hiding this comment.
[Note to reviewer]: The arch mapping is the same for debs and artefacts for the store so these 2 functions can be used here. However, at this point they are not really "deb" specific. And it looks surprising to import the deb package here. What about extracting these functions into a arch package?
letFunny
left a comment
There was a problem hiding this comment.
This looks very very good Paul. I tried to do as much of a thorough analysis as I could and I don't see anything major, just a couple of comments about backwards compatibility. Thanks!
| reader, info, err := src.archive.Fetch(src.pkg.RealName) | ||
| if err != nil { | ||
| return err | ||
| // The package metadata returned by the store is not yet recorded |
There was a problem hiding this comment.
It is hard for me to comment here without knowing all details so I will take this as already-discussed gospel :)
There was a problem hiding this comment.
I reworked this comment a bit, the first sentence was out of place. Regarding the heuristic to build the channel: this is the current agreement but is subject to change. So I am doing it in a single place, in a simple way, with a comment so it will be easy to change if we need to.
| type RunOptions struct { | ||
| Selection *setup.Selection | ||
| Archives map[string]archive.Archive | ||
| Stores map[string]store.Store |
There was a problem hiding this comment.
Per the comments in the other PR, this distinction makes sense and makes the code simpler in a way. Normally in Go you don't define generics to abstract over a type (i.e. a store) but to abstract over an operation, in this case there are clearly two:
- How to fetch a package.
- Record information from the package in manifest.
As we discussed in the previous PR, this was contemplated but it turned out to be much more complex AFAIK. I also see the difficulty in Go where the type system is not powerful so it is hard to model fetching where archive.fetch(name) takes only one args vs three in store.fetch(name, track, risk) for example.
There was a problem hiding this comment.
We agree. I could have reworked the Archive interface to have something more generic, used for archives and stores, but in reality they are different enough that 2 different interfaces made more sense.
As I go through the complete implementation I may discover that this new interface must be tweaked.
| } | ||
| pkgSources[pkg.Name] = &pkgSourceInfo{ | ||
| // TODO: Fill with the live store handle when store support is implemented. | ||
| arch: storeHandle.Options().Arch, |
There was a problem hiding this comment.
I was hesitant in #15 because with only that diff it looked weird. However, it looks okay here. You can postpone the refactor for arch.
I am not sure the other PR will not get comments about this so it might be helpful to copy the idea of this diff in a comment in canonical/chisel just in case.
There was a problem hiding this comment.
Good point. I added https://github.com/upils/chisel/pull/15/files#r3479609705 (and did the same on the PR submitted to the main repo)
| } | ||
|
|
||
| // Download the package. | ||
| err = validateDownloadURL(rev.Download.URL, s.downloadHost) |
There was a problem hiding this comment.
This is good to prevent upstream errors impacting security. It also makes it so that a change in the urls will require a new major version of Chisel to be released. This is a similar problem to public keys in a way; we solved that one by including it in the release itself so that the release can evolve if the keys changes and they are not hardcoded in Chisel. Is that the experience we want here?
There was a problem hiding this comment.
This is a good point. I discussed it with Bowen before moving forward on this: these URLs redirect to CDN URLs, so I understand they are as likely as the API host to change (so rather unlikely). I will bring that topic up to Gustavo tomorrow as I agree this would create a need of a breaking change.
letFunny
left a comment
There was a problem hiding this comment.
Looks good to me Paul, let's see how it goes :D
| // no release for the requested channel and architecture. | ||
| func (s *binStore) resolveRevision(name, track, risk string) (*binRevision, error) { | ||
| if !nameExp.MatchString(name) { | ||
| return nil, fmt.Errorf("invalid package name %q", name) |
There was a problem hiding this comment.
That makes sense as a measure to sanitize requests to the store as you say. The only problem is that now there are two regexps that have to be valid against the same name which create some bugs in the duplication. Anyway, I think it is an acceptable trade-off.
| {"amd64", ""}, | ||
| {"arm64", ""}, | ||
| {"invalid", "invalid package architecture: invalid"}, | ||
| {"Valid amd64", "amd64", ""}, |
There was a problem hiding this comment.
I reworked the test to make it more generic, only keeping one non-failing case and integrating the standalone TestOpenUnsupportedKind.
This PR enables Chisel fetching packages from the Store API in addition to Debian archives. Store-backed packages are declared in release YAML with
store: <name>anddefault-track: <track>, and are resolved via the store's/v2/revisions/resolveendpoint.Channel selection: the effective track is
<default-track>-<store-version>(e.g.3.1-26.10), composed from the package'sdefault-trackand the store's releaseversionfield. Risk defaults tostablewhen unspecified. The slicer owns this composition; the store package is a pure API client with no release knowledge.Security: download URLs are validated for HTTPS and exact host match against
api.snapcraft.io(production) orapi.staging.snapcraft.io(staging, enabled viaCHISEL_BIN_STAGING). The host is resolved atOpentime and fixed for the store's lifetime.Caching: packages are cached by SHA3-384 digest, consistent with the archive's cache pattern. The digest is verified on write.
Current limitation: store packages can be fetched and cached but not yet extracted — the slicer fails with a clear error at extraction time. Manifest recording of store packages is also deferred.